#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Datetime: 2021/12/05 13:14
# @Author  : CHEN Wang
# @Site    :
# @File    : okex.py
# @Software: PyCharm

"""
脚本说明: 从OKEX api上获取相关信息
OKEx api
具体api文档参考:
https://www.okx.com/docs-v5/zh/
https://www.okx.com/demo-trading-explorer/v5/zh
https://github.com/jane-cloud/Open-API-SDK-V5
"""

import requests
import json
import datetime
import time
from dateutil.tz import tzutc
import pandas as pd
import numpy as np
from requests.exceptions import ConnectTimeout, SSLError, ReadTimeout, ConnectionError
from retrying import retry
from urllib.parse import urljoin
from quant_researcher.quant.project_tool.wrapper_tools.common_wrappers import deco_retry
from quant_researcher.quant.project_tool.time_tool import get_today, datetime_to_timestamp, timestamp_to_str, str_to_timestamp
from quant_researcher.quant.project_tool.logger.my_logger import LOG

TIMEOUT = 10
ILOVECHINA = "同学！！你知道什么叫做科学上网么？ 如果你不知道的话，那么就加油吧！蓝灯，喵帕斯，VPS，阴阳师，v2ray，随便什么来一个！我翻墙我骄傲！"
OKEx_base_url = "https://www.okex.com/"

"""
QUANTAXIS 和 okex 的 frequency 常量映射关系
"""
OKEx2QA_FREQUENCY_DICT = {"60": '1min', "300": '5min', "900": '15min', "1800": '30min', "3600": '60min', "86400": 'day'}
"""
OKEx 只允许一次获取 200bar，时间请求超过范围则只返回最新200条
"""
# 时间粒度granularity必须是[60 180 300 900 1800 3600 7200 14400 21600 43200 86400 604800 2678400 8035200 16070400 31536000]]中的任一值，否则请求将被拒绝。
# 这些值分别对应的是[1min 3min 5min 15min 30min 1hour 2hour 4hour 6hour 12hour 1day 1week 1 month 3 months 6 months and 1 year]的时间段。
FREQUENCY_SHIFTING = {"60": 12000, "300": 60000, "900": 180000, "1800": 360000, "3600": 720000, "86400": 17280000}


def format_okex_data_fields(datas, symbol, frequency):
    """
    # 归一化数据字段，转换填充必须字段，删除多余字段
    参数名 	类型 	描述
    time 	String 	开始时间
    open 	String 	开盘价格
    high 	String 	最高价格
    low 	String 	最低价格
    close 	String 	收盘价格
    volume 	String 	交易量
    """

    column_names = ['time', 'open', 'high', 'low', 'close', 'volume', ]
    frame = pd.DataFrame(datas, columns=column_names)
    frame['symbol'] = 'OKEX.{}'.format(symbol)
    # GMT+0 String 转换为 UTC Timestamp
    frame['time_stamp'] = pd.to_datetime(frame['time']).astype(np.int64) // 10 ** 9
    # UTC时间转换为北京时间 
    frame['datetime'] = pd.to_datetime(frame['time_stamp'], unit='s').dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai')
    frame['date'] = frame['datetime'].dt.strftime('%Y-%m-%d')
    frame['date_stamp'] = pd.to_datetime(frame['date']).dt.tz_localize('Asia/Shanghai').astype(np.int64) // 10 ** 9
    frame['datetime'] = frame['datetime'].dt.strftime('%Y-%m-%d %H:%M:%S')
    frame['created_at'] = int(time.mktime(datetime.datetime.now().utctimetuple()))
    frame['updated_at'] = int(time.mktime(datetime.datetime.now().utctimetuple()))
    frame.drop(['time'], axis=1, inplace=True)
    frame['trade'] = 1
    frame['amount'] = frame.apply(lambda x: float(x['volume']) * (float(x['open']) + float(x['close'])) / 2, axis=1)
    if (frequency not in ['1day', 'day', '86400', '1d']):
        frame['type'] = OKEx2QA_FREQUENCY_DICT[frequency]
    return frame


@retry(stop_max_attempt_number=3, wait_random_min=50, wait_random_max=100)
def QA_fetch_okex_symbols():
    """
    获取交易币对的列表，查询各币对的交易限制和价格步长等信息。
    限速规则：20次/2s
    HTTP请求 GET/api/spot/v3/instruments
    """
    url = urljoin(OKEx_base_url, "/api/spot/v3/instruments")
    retries = 1
    datas = list()
    while (retries != 0):
        try:
            req = requests.get(url, timeout=TIMEOUT)
            retries = 0
        except (ConnectTimeout, ConnectionError, SSLError, ReadTimeout):
            retries = retries + 1
            if (retries % 6 == 0):
                print(ILOVECHINA)
            print("Retry /api/spot/v3/instruments #{}".format(retries - 1))
            time.sleep(0.5)

        if (retries == 0):
            # 成功获取才处理数据，否则继续尝试连接
            symbol_lists = json.loads(req.content)
            if len(symbol_lists) == 0:
                return []
            for symbol in symbol_lists:
                datas.append(symbol)

    return datas


@retry(stop_max_attempt_number=3, wait_random_min=50, wait_random_max=100)
def QA_fetch_okex_kline_with_auto_retry(symbol, start_time, end_time, frequency, ):
    """
    Get the latest symbol‘s candlestick data raw method
    获取币对的K线数据。K线数据按请求的粒度分组返回，k线数据最多可获取200条(说明文档中2000条系错误)。
    限速规则：20次/2s
    HTTP请求 GET/api/spot/v3/instruments/<instrument_id>/candles
    """
    url = urljoin(OKEx_base_url, "/api/spot/v3/instruments/{:s}/history/candles".format(symbol))
    retries = 1
    while (retries != 0):
        try:
            start_epoch = datetime.datetime.fromtimestamp(start_time, tz=tzutc())
            end_epoch = datetime.datetime.fromtimestamp(end_time, tz=tzutc())
            req = requests.get(
                url,
                params={
                    "end": start_epoch.isoformat().replace("+00:00", ".000Z"),  # Z结尾的ISO时间 String
                    "start": end_epoch.isoformat().replace("+00:00", ".000Z"),  # Z结尾的ISO时间 String
                    # "start": start_epoch.isoformat().replace("+00:00", ".000Z"),   # Z结尾的ISO时间 String
                    # "end": end_epoch.isoformat() .replace("+00:00", ".000Z"),       # Z结尾的ISO时间 String
                    "granularity": frequency,
                },
                timeout=TIMEOUT
            )
            # 防止频率过快被断连
            time.sleep(0.5)
            retries = 0
        except (ConnectTimeout, ConnectionError, SSLError, ReadTimeout):
            retries = retries + 1
            if (retries % 6 == 0):
                print(ILOVECHINA)
            print("Retry /api/spot/v3/instruments #{}".format(retries - 1))
            time.sleep(0.5)

        if (retries == 0):
            # 成功获取才处理数据，否则继续尝试连接
            msg_dict = json.loads(req.content)
            if ('error_code' in msg_dict):
                print('Error', msg_dict)
                return None
            return msg_dict

    return None


def QA_fetch_okex_kline(symbol, start_time, end_time, frequency, callback_func=None):
    """
    Get the latest symbol‘s candlestick data
    时间倒序切片获取算法，是各大交易所获取1min数据的神器，因为大部分交易所直接请求跨月跨年的1min分钟数据
    会直接返回空值，只有将 start_epoch，end_epoch 切片细分到 200/300 bar 以内，才能正确返回 kline，
    火币和binance，OKEx 均为如此，直接用跨年时间去直接请求上万bar 的 kline 数据永远只返回最近200条数据。
    """
    datas = list()
    reqParams = {}
    reqParams['from'] = end_time - FREQUENCY_SHIFTING[frequency]
    reqParams['to'] = end_time

    while (reqParams['to'] > start_time):
        if ((reqParams['from'] > datetime_to_timestamp())) or \
                ((reqParams['from'] > reqParams['to'])):
            # 出现“未来”时间，一般是默认时区设置，或者时间窗口滚动前移错误造成的
            LOG.info(
                'A unexpected \'Future\' timestamp got, Please check self.missing_data_list_func param \'tzlocalize\' set. More info: {:s}@{:s} at {:s} but current time is {}'
                    .format(
                    symbol,
                    frequency,
                    timestamp_to_str(reqParams['from']),
                    timestamp_to_str(datetime_to_timestamp())
                )
            )
            # 跳到下一个时间段
            reqParams['to'] = int(reqParams['from'] - 1)
            reqParams['from'] = int(reqParams['from'] - FREQUENCY_SHIFTING[frequency])
            continue

        klines = QA_fetch_okex_kline_with_auto_retry(symbol, reqParams['from'], reqParams['to'], frequency)

        if (klines is None) or (len(klines) == 0) or ('error' in klines):
            # 出错放弃
            break

        reqParams['to'] = int(reqParams['from'] - 1)
        reqParams['from'] = int(reqParams['from'] - FREQUENCY_SHIFTING[frequency])

        if (klines is None) or ((len(datas) > 0) and (klines[-1][0] == datas[-1][0])):
            # 没有更多数据
            break

        datas.extend(klines)

        if (callback_func is not None):
            frame = format_okex_data_fields(klines, symbol, frequency)
            callback_func(frame, OKEx2QA_FREQUENCY_DICT[frequency])

    if len(datas) == 0:
        return None

    # 归一化数据字段，转换填充必须字段，删除多余字段
    frame = format_okex_data_fields(datas, symbol, frequency)
    return frame


def QA_fetch_okex_kline_min(symbol, start_time, end_time, frequency, callback_func=None):
    """
    Get the latest symbol‘s candlestick data with time slices
    时间倒序切片获取算法，是各大交易所获取1min数据的神器，因为大部分交易所直接请求跨月跨年的1min分钟数据
    会直接返回空值，只有将 start_epoch，end_epoch 切片细分到 200/300 bar 以内，才能正确返回 kline，
    火币和binance，OKEx 均为如此，用上面那个函数的方式去直接请求上万bar 的分钟 kline 数据是不会有结果的。
    """
    reqParams = {}
    reqParams['from'] = end_time - FREQUENCY_SHIFTING[frequency]
    reqParams['to'] = end_time

    requested_counter = 1
    datas = list()
    while (reqParams['to'] > start_time):
        if ((reqParams['from'] > datetime_to_timestamp())) or \
                ((reqParams['from'] > reqParams['to'])):
            # 出现“未来”时间，一般是默认时区设置，或者时间窗口滚动前移错误造成的
            LOG.info(
                'A unexpected \'Future\' timestamp got, Please check self.missing_data_list_func param \'tzlocalize\' set. More info: {:s}@{:s} at {:s} but current time is {}'
                    .format(
                    symbol,
                    frequency,
                    timestamp_to_str(reqParams['from']),
                    timestamp_to_str(datetime_to_timestamp())
                )
            )
            # 跳到下一个时间段
            reqParams['to'] = int(reqParams['from'] - 1)
            reqParams['from'] = int(reqParams['from'] - FREQUENCY_SHIFTING[frequency])
            continue

        klines = QA_fetch_okex_kline_with_auto_retry(symbol, reqParams['from'], reqParams['to'], frequency, )
        if (klines is None) or (len(klines) == 0) or ('error' in klines):
            # 出错放弃
            break

        reqParams['to'] = int(reqParams['from'] - 1)
        reqParams['from'] = int(reqParams['from'] - FREQUENCY_SHIFTING[frequency])

        if (callback_func is not None):
            frame = format_okex_data_fields(klines, symbol, frequency)
            callback_func(frame, OKEx2QA_FREQUENCY_DICT[frequency])

        if (len(klines) == 0):
            return None


@deco_retry(retry=50, retry_sleep=15)
def fetch_okex_margin_interest_rate():
    """
    https://www.okx.com/fees/margin
    获取okex上的杠杆借贷费率
    """

    header = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36'}
    today = get_today(marker='with_n_dash')
    end = str_to_timestamp(today) * 1000
    url = f'https://www.okx.com/priapi/v5/public/interest-rate-loan-quota?t={end}'
    res = requests.get(url, headers=header, timeout=15)
    datas = json.loads(res.content)
    datas = datas['data'][0]['basic']
    df = pd.DataFrame(datas)
    df['end_date'] = today

    return df


@deco_retry(retry=50, retry_sleep=15)
def fetch_okex_usdtusd_premium(frequnecy='1d'):
    """
    从https://www.okx.com/markets/data-list 获取USDT场外兑USD折溢价数据

    :return:
    """
    header = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36'}
    # date = get_today(marker='with_n_dash')
    timestamp_ = str_to_timestamp('2010-01-01') * 1000
    frequnecy_dict = {'5m': 0, '1h': 1, '1d': 2}
    url = f'https://www.okx.com/v3/c2c/tickers/premiumRate/USDT?t={timestamp_}&interval={frequnecy_dict[frequnecy]}'
    # date = timestamp_to_str(1663905947151/1000, fmt='%Y-%m-%d %H:%M:%S', tz_str='+0800')
    res = requests.get(url, headers=header, timeout=15)
    df = pd.DataFrame(json.loads(res.content)['data'])
    df['timestamps'] = df['timestamps'] / 1000
    df['datetime'] = df['timestamps'].apply(timestamp_to_str, fmt='%Y-%m-%d %H:%M:%S', tz_str='+0000')
    df['date'] = df['timestamps'].apply(timestamp_to_str, fmt='%Y-%m-%d', tz_str='+0000')
    df.rename(columns={'ratios': 'premium'}, inplace=True)

    return df


@deco_retry(retry=50, retry_sleep=15)
def fetch_okex_usdtcny_premium():
    """
    从https://www.okx.com/markets/data-list 获取USDT场外兑CNY折溢价数据

    :return:
    """

    header = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36'}
    timestamp_ = int(time.time() * 1000)
    datetime_ = timestamp_to_str(timestamp_/1000, fmt='%Y-%m-%d %H:%M:%S', tz_str='+0000')
    date_ = timestamp_to_str(timestamp_/1000, fmt='%Y-%m-%d', tz_str='+0000')

    ticker_url = f'https://www.okx.com/v3/c2c/otc-ticker?t={timestamp_}&baseCurrency=USDT&quoteCurrency=CNY'
    orderbook_url = f'https://www.okx.com/v3/c2c/tradingOrders/books?t={timestamp_}&quoteCurrency=cny&baseCurrency=usdt&side=all' \
                    f'&paymentMethod=all&userType=all&showTrade=false&receivingAds=false&showFollow=false&showAlreadyTraded=false&isAbleFilter=false'

    res1 = requests.get(ticker_url, headers=header, timeout=30)
    df_ticker = pd.DataFrame(json.loads(res1.content)['data'], index=[0])
    df_ticker.rename(columns={'otcTicker': 'usdtcny'}, inplace=True)
    df_ticker['usdtcny'] = df_ticker['usdtcny'].astype(float)
    df_ticker['timestamp'] = timestamp_
    df_ticker['datetime'] = datetime_
    df_ticker['date'] = date_

    res2 = requests.get(orderbook_url, headers=header, timeout=30)
    df_buy = pd.DataFrame(json.loads(res2.content)['data']['sell'])  # 这里的数据正好是相反的
    df_sell = pd.DataFrame(json.loads(res2.content)['data']['buy'])
    df_orderbook = pd.concat([df_buy, df_sell], axis=0)
    df_orderbook.rename(columns={'price': 'usdtcny'}, inplace=True)
    df_orderbook['usdtcny'] = df_orderbook['usdtcny'].astype(float)
    df_orderbook['timestamp'] = timestamp_
    df_orderbook['spread'] = (df_buy['price'].astype(float).min() - df_sell['price'].astype(float).max()) / df_sell['price'].astype(float).max()
    df_orderbook['datetime'] = datetime_
    df_orderbook['date'] = date_

    return df_ticker, df_orderbook


if __name__ == '__main__':
    # start = time.mktime(datetime.datetime(2015, 1, 1, 0, 0, 0, tzinfo=tzutc()).timetuple())
    # end = time.mktime(datetime.datetime(2022, 3, 6, 14, 0, 0, tzinfo=tzutc()).timetuple())
    #
    # data = QA_fetch_okex_kline("BTC-USDT", start, end, '86400')
    # file_path = os.path.join(DATA_DIR)
    # file_name = os.path.join(file_path, f'okex_ohlcv')
    # data.to_excel(f'{file_name}.xlsx')
    #
    # # url = urljoin(OKEx_base_url, "/api/swap/v3/instruments/BTC-USD-SWAP/open_interest")
    # url = urljoin("https://aws.okex.com/", "/api/swap/v3/instruments/BTC-USD-SWAP/open_interest")
    # print(url)
    # a = requests.get(url)
    # print(a.content)
    # print(json.loads(a.content))

    # 测试fetch_okex_margin_interest_rate
    # df = fetch_okex_margin_interest_rate()

    # 测试fetch_okex_usdtusd_premium
    # df = fetch_okex_usdtusd_premium(frequnecy='5m')

    # 测试fetch_okex_usdtcny_premium
    fetch_okex_usdtcny_premium()
